import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * A command line application for counting words with support for custom
 * word-break characters.
 */
public class WordCount {

    /**
     * Implements the core word counting algorithm.
     */
    private static class Counter {
        private static final int EOF = -1;

        private static final byte[] VAR_BREAK_CHARS = " :/.,+*()~&%$#!`\"{}|^[]\\@?<>=;"
                .getBytes();

        private boolean delimiters[] = new boolean[256];

        private InputStream input;

        private long processedBytes;

        public Counter(InputStream input, String newDelimiters) {
            this.input = input;
            setDefaultDelimiters();

            if (newDelimiters != null) {
                // Undo (only) the default variable break chars.
                for (int i = 0; i < VAR_BREAK_CHARS.length; i++) {
                    delimiters[VAR_BREAK_CHARS[i]] = false;
                }

                // Apply the user's break chars.
                byte delimiterBytes[] = newDelimiters.getBytes();
                for (int i = 0; i < delimiterBytes.length; i++) {
                    delimiters[(int) delimiterBytes[i] & 0xFF] = true;
                }
            }
        }

        public int count() throws IOException {
            return this.count(false);
        }

        public int count(boolean countBytes) throws IOException {
            boolean nonZeroLengthWord = false;
            int numWords = 0;
            int character = 0;

            while ((character = this.input.read()) != EOF) {
                if (countBytes) {
                    processedBytes++;
                }
                if (delimiters[character]) {
                    if (nonZeroLengthWord) {
                        numWords++;
                        nonZeroLengthWord = false;
                    }
                } else {
                    nonZeroLengthWord = true;
                }
            }

            return nonZeroLengthWord ? numWords + 1 : numWords;
        }

        public long getProcessedBytes() {
            return this.processedBytes;
        }

        private void setDefaultDelimiters() {
            for (int i = 0; i <= 037; i++) { // 0-31 decimal
                delimiters[i] = true;
            }
            for (int i = 0177; i <= 0377; i++) { // 127-255 decimal
                delimiters[i] = true;
            }

            // Apply default variable break chars.
            for (int i = 0; i < VAR_BREAK_CHARS.length; ++i) {
                delimiters[VAR_BREAK_CHARS[i]] = true;
            }
        }
    }

    /**
     * Indicates an invalid command line was supplied.
     */
    private static class WordCountBadCommandLineException extends
            WordCountException {
        public WordCountBadCommandLineException() {
            super("ERROR: ill-formed command");
        }
    }

    /**
     * Base class for application exceptions.
     */
    private static class WordCountException extends Exception {
        public WordCountException(String msg) {
            super(msg);
        }
    }

    /**
     * Indicates the file named on the command line was inaccessible (for
     * example, due to lack of access).
     */
    private static class WordCountFileNotAccessibleException extends
            WordCountException {
        public WordCountFileNotAccessibleException() {
            super("ERROR: unable to read file");
        }
    }

    /**
     * Indicates the file named on the command line was not found.
     */
    private static class WordCountFileNotFoundException extends
            WordCountException {
        public WordCountFileNotFoundException() {
            super("ERROR: unable to process input file");
        }
    }

    public static void main(String[] args) {
        boolean success = wordCount(args);
        System.exit(success ? 0 : 1);
    }

    public static boolean wordCount(String[] args) {
        try {
            wordCountImpl(args);
            return true;
        } catch (WordCountException e) {
            System.err.println(e.getMessage());
        } catch (Throwable e) {
            System.err.println("ERROR: program terminated unexpectedly");
        }
        return false;
    }

    private static void wordCountImpl(String[] args)
            throws WordCountBadCommandLineException,
            WordCountFileNotFoundException,
            WordCountFileNotAccessibleException, IOException {
        long startTime = 0;
        String filename = null;
        String delimiters = null;

        Counter cnt;

        boolean print_metadata = (System.getProperty("wc.stats") != null);

        if (print_metadata) {
            System.currentTimeMillis();
        }

        // Parse the command line.
        switch (args.length) {
        case 0:
            // No custom delimiters and read from stdin.
            break;

        case 1:
            // No custom delimeters and read from a file.
            filename = args[0];
            break;

        case 2:
            // Custom delimeters and read from stdin.
            if (!args[0].equals("-b")) {
                throw new WordCountBadCommandLineException();
            }
            delimiters = args[1];
            break;

        case 3:
            // Custom delimeters and read from a file.
            if (!args[0].equals("-b")) {
                throw new WordCountBadCommandLineException();
            }
            delimiters = args[1];
            filename = args[2];
            break;

        default:
            // Anything else is bad syntax.
            throw new WordCountBadCommandLineException();
        }

        // Count the words.
        InputStream input;
        if (filename != null) {
            File file = new File(filename);
            if (!file.exists()) {
                throw new WordCountFileNotFoundException();
            }
            if (!file.isFile() || !file.canRead()) {
                throw new WordCountFileNotAccessibleException();
            }
            input = new FileInputStream(file);
        } else {
            input = System.in;
        }

        BufferedInputStream bufferedIn = new BufferedInputStream(input);
        cnt = new Counter(bufferedIn, delimiters);
        int numWords = cnt.count(print_metadata);
        System.out.println(numWords);
        input.close();

        if (print_metadata) {
            long stopTime = System.currentTimeMillis();
            long elapsedTime = stopTime - startTime;

            System.out.println("File: " + filename);
            System.out.println("Deli: " + delimiters);

            System.out.println("Execution time: " + (double) elapsedTime
                    / 1000.0 + " seconds");

            long length = cnt.getProcessedBytes();
            double d = (double) length / ((double) elapsedTime / 1000.0);

            System.out.println("Throughput: " + d + "B/s");
        }
    }
}
